package com.xbongbong.paas.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xbongbong.paas.constant.PaasConstant;
import com.xbongbong.paas.gateway.config.GatewayConstant;
import com.xbongbong.paas.gateway.config.GatewayOuterConstant;
import com.xbongbong.paas.gateway.util.DataBufferUtil;
import com.xbongbong.paas.redis.PaasRedisHelper;
import com.xbongbong.paas.redis.RedisPrefixConstant;
import com.xbongbong.paas.toolbox.exception.XbbException;
import com.xbongbong.paas.toolbox.util.DigestUtil;
import com.xbongbong.paas.toolbox.util.StringUtil;
import com.xbongbong.paas.toolbox.wrap.XbbResponse;
import com.xbongbong.pro.config.ProBaseConfig;
import com.xbongbong.pro.enums.ErrorCodeEnum;
import com.xbongbong.pro.enums.errorcodes.ErrorCode;
import com.xbongbong.pro.enums.errorcodes.SystemErrorCodeEnum;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

import static com.xbongbong.paas.gateway.filter.CacheBodyGatewayFilter.CACHE_REQUEST_BODY_OBJECT_KEY;

/**
 * 认证过滤器
 * @author kaka
 * @time 2018-8-1 9:22
 */
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {

    private static final Log LOG = LogFactory.getLog(AuthenticationFilter.class);

    static final String XBB_GATE_CACHE_REQUEST_BODY_JSON_KEY = "xbbCachedRequestBodyJson";

    @Resource
    private PaasRedisHelper paasRedisHelper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        // 判断请求的链接是否不需要校验
        for (String noFilterPrefix : GatewayConstant.NO_FILTER_PATH_PREFIX) {
            if(path.startsWith(noFilterPrefix)) {
                return chain.filter(exchange);
            }
        }
        Boolean isOuterLink = false;
        for (String noFilterPrefix : GatewayOuterConstant.OUTER_NO_FILTER_PATH_PREFIX) {
            if(path.startsWith(noFilterPrefix)) {
                isOuterLink = true;
            }
        }
        //签名
        String sign = request.getHeaders().getFirst("sign");
        HttpHeaders headers = exchange.getResponse().getHeaders();
        // 指定错误返回的Content-Type
//        headers.add("Content-Type", PaasConstant.RESPONSE_CONTENT_TYPE);
        headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
        if(!isOuterLink && StringUtil.isEmpty(sign)) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer().write(formatVerifyError(SystemErrorCodeEnum.API_ERROR_100012).getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        }

        Flux<DataBuffer> cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
        if (cachedBody == null) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer().write(formatVerifyError(SystemErrorCodeEnum.API_ERROR_100068).getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        }
        // 将缓存请求体转为String类型
        String requestBodyStr = DataBufferUtil.toRaw(cachedBody);
        LOG.debug("网关收到请求，请求内容为：" + requestBodyStr);

        // 获取 requestJson
        JSONObject requestJson;
        try {
            requestJson = getRequestJsonObj(requestBodyStr);
        } catch (XbbException xe) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer().write(formatVerifyError(xe).getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        }

        if(isOuterLink) {
            // 设置缓存，后续的过滤器能够直接取到json
            exchange.getAttributes().put(XBB_GATE_CACHE_REQUEST_BODY_JSON_KEY, requestJson);
            return chain.filter(exchange);
        }
        // 获取 accessToken
        String accessToken;
        try {
            accessToken = getXbbAccessToken(requestJson);
        } catch (XbbException xe) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer().write(formatVerifyError(xe).getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        } catch (Exception e) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer().write(formatVerifyError(SystemErrorCodeEnum.API_ERROR_100001).getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        }

        /*
         * 是否需要完整性验证
         */
        boolean checkFlag = checkFlag(request, requestJson);
        if (checkFlag) {
            String calculateSign = DigestUtil.Encrypt(requestBodyStr + accessToken, "SHA-256");
            if (!sign.equalsIgnoreCase(calculateSign)) {
                LOG.warn("签名错误， 参数为 ： " + requestBodyStr);
                LOG.warn("accessToken为 ： " + accessToken);
                LOG.warn("calculateSign为 ： " + calculateSign);
                LOG.warn("sign为 ： " + sign);
                // 完整性验证失败
                DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer().write(formatVerifyError(SystemErrorCodeEnum.API_ERROR_100012).getBytes(StandardCharsets.UTF_8));
                return exchange.getResponse().writeWith(Mono.just(dataBuffer));
            }
        }

        // 设置缓存，后续的过滤器能够直接取到json
        exchange.getAttributes().put(XBB_GATE_CACHE_REQUEST_BODY_JSON_KEY, requestJson);
        //封装request，传给下一级
        return chain.filter(exchange);
    }

    /**
     *  比AuthorizationFilter优先级高1
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1001;
    }

    //==================== private method ====================

    /**
     * 是否要进行签名验证，目前除了本地调试可以跳过验证，其他情况都需验证
     * 本地调试要符合
     * 1.参数中包含frontDev参数并且等于{@link PaasConstant#FRONT_DEV_FLAG}
     * 2.ip地址为192.168开头的内网地址，或者ipV4或ipV6的本地自回环地址
     * 本地调试时可以跳过签名验证
     * @param request
     * @param requestJson
     * @return
     */
    private boolean checkFlag(ServerHttpRequest request, JSONObject requestJson) {
        boolean checkFlag = true;
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        if (remoteAddress != null) {
            InetAddress inetAddress = remoteAddress.getAddress();
            String ipAddress = inetAddress.getHostAddress();
            String frontDev = requestJson.getString("frontDev");
            // 是否是本地ip
            boolean isLocalIp = (ipAddress != null)
                    && (ipAddress.startsWith(PaasConstant.LAN_IP_PREFIX) || ipAddress.startsWith(PaasConstant.LOCAL_HOST_IPV4) || ipAddress.startsWith(PaasConstant.LOCAL_HOST_IPV6));
            /*
             * 本地调试要符合
             * 1.参数中包含frontDev参数并且等于{@link PaasConstant#FRONT_DEV_FLAG}
             * 2.ip地址为192.168开头的内网地址，或者ipV4或ipV6的本地自回环地址 或者是测试环境也允许跳过验证
             * 本地调试时可以跳过签名验证
             */
            boolean localTuning = (frontDev != null)
                    && PaasConstant.FRONT_DEV_FLAG.equals(frontDev)
                    && (ProBaseConfig.getProjectName().startsWith("xbbProTest") || isLocalIp);

            if (localTuning) {
                checkFlag = false;
                LOG.info("本地调试，来源IP为：" + ipAddress);
            }
        }
        return checkFlag;
    }

    /**
     * 构建错误返回数据，目前格式如下
     * {
     *     "code": 100012,
     *     "msg": "登录验证过期，请重新登录",
     *     "empty": true,
     *     "success": false,
     *     "totalCount": 0
     * }
     *
     * @author kaka
     * @time 2018-8-1 10:55
     */
    private String formatVerifyError(ErrorCodeEnum errorCodeEnum) {
        XbbResponse verifyError = new XbbResponse(errorCodeEnum);
        return JSON.toJSONString(verifyError);
    }

    private String formatVerifyError(ErrorCode errorCode) {
        XbbResponse verifyError = new XbbResponse(errorCode);
        return JSON.toJSONString(verifyError);
    }

    /**
     * 通过 XbbException 构建错误返回数据
     * @author kaka
     * @time 2018-9-12 14:23
     */
    private String formatVerifyError(XbbException xe) {
        XbbResponse verifyError = new XbbResponse(xe);
        return JSON.toJSONString(verifyError);
    }

    /**
     * 获取 requestBodyStr 对应的 JSONObject
     * @param requestBodyStr 请求数据字符串
     * @return requestBodyStr 转化后的 JSONObject
     */
    private JSONObject getRequestJsonObj(String requestBodyStr) throws XbbException {
        if (StringUtil.isEmpty(requestBodyStr)) {
            // 输入参数格式错误，参数为空
            throw new XbbException(SystemErrorCodeEnum.API_ERROR_100015);
        }

        JSONObject requestJson;
        try {
            requestJson =JSONObject.parseObject(requestBodyStr);
        } catch (Exception e) {
            // 输入参数格式错误，无法转为json
            throw new XbbException(SystemErrorCodeEnum.API_ERROR_100015);
        }

        if (requestJson == null) {
            // 输入参数格式错误，转化为空json
            throw new XbbException(SystemErrorCodeEnum.API_ERROR_100015);
        }

        return requestJson;
    }

    /**
     * 根据请求参数获取 accessToken
     * @param requestJson 请求数据对应的JSONObject
     * @return 获取对应的 corpid 和 userId 对应的 accessToken
     * @throws XbbException 业务异常
     */
    private String getXbbAccessToken(JSONObject requestJson) throws XbbException {

        // 验证逻辑
        String corpid = requestJson.getString("corpid");
        String userId = requestJson.getString("userId");
        String platform = requestJson.getString("platform");
        if (StringUtil.isEmpty(corpid) || StringUtil.isEmpty(userId) || StringUtil.isEmpty(platform)) {
            //参数缺失，不能没有corpid 和 userId
            throw new XbbException(SystemErrorCodeEnum.API_ERROR_100012);
        }

        return paasRedisHelper.getValue(RedisPrefixConstant.XBB_ACCESS_TOKEN, userId + "____" + corpid);
    }
}

